package http.secure.common.filter;

import http.secure.common.constant.RequestTemplate;
import http.secure.common.constant.SecureConstant;
import http.secure.common.exception.InvalidRequestException;
import http.secure.common.exception.RequestNotSupportedException;
import http.secure.common.util.Assert;
import http.secure.common.util.SMHelper;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import lombok.extern.slf4j.Slf4j;
import org.dromara.hutool.core.array.ArrayUtil;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.http.server.servlet.ServletUtil;
import org.dromara.hutool.json.JSONConfig;
import org.dromara.hutool.json.JSONUtil;
import org.springframework.http.MediaType;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Secure servlet request wrapper
 */
@Slf4j
final class SecureRequestWrapper extends HttpServletRequestWrapper {
    /**
     * Request parameters
     */
    private Map<String, String[]> params = new LinkedHashMap<>();
    /**
     * Request body
     */
    private Map<String, Object> body = new LinkedHashMap<>();
    /**
     * Link timestamp
     */
    private String timestamp;
    /**
     * Link signature
     */
    private String signature;

    /**
     * Secure servlet request wrapper
     *
     * @param request {@link ServletRequest}
     */
    public SecureRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.initialize();
    }

    private void initialize() throws IOException {
        if (ServletUtil.isPostMethod(this)) {
            MediaType mediaType = MediaType.parseMediaType(getContentType());
            boolean compatible = MediaType.APPLICATION_JSON.isCompatibleWith(mediaType);
            Assert.isTrue(compatible, () -> new RequestNotSupportedException(RequestTemplate.NOT_SUPPORTED_PAYLOAD, body));
            String read = IoUtil.read(super.getInputStream(), Charset.forName(getCharacterEncoding()));
            this.body = JSONUtil.parseObj(read, JSONConfig.of().setIgnoreNullValue(false)).toMap(String.class, Object.class);
        }
        this.params.putAll(super.getParameterMap());
        // print-log
        log.debug("params: {}", params);
        log.debug("body: {}", body);
    }

    /**
     * try to decrypt all data
     *
     * @throws InvalidRequestException decryption error
     */
    void decrypt(String key, String iv) {
        // Decrypt FormData
        params.forEach((k, v) -> decrypt(key, iv, k, v));
        // Decryption request body
        body.forEach((k, v) -> decrypt(key, iv, k, v));
        // Find signature
        String signature = findSignature();
        this.signature = signature.substring(0, 64);
        this.timestamp = signature.substring(64);
    }

    /**
     * Decrypt FormData
     */
    private void decrypt(String sm4key, String iv, String key, String[] values) {
        String lm = SecureConstant.LM, rm = SecureConstant.RM;
        if (ArrayUtil.length(values) == 1 && StrUtil.isWrap(values[0], lm, rm)) {
            values[0] = SMHelper.sm4Decrypt(StrUtil.unWrap(values[0], lm, rm), sm4key, iv);
        }
        params.put(key, values);
    }

    /**
     * Decryption request body
     */
    private void decrypt(String sm4key, String iv, String key, Object value) {
        if (StrUtil.isWrap(String.valueOf(value), SecureConstant.LM, SecureConstant.RM)) {
            String unWrap = StrUtil.unWrap(value.toString(), SecureConstant.LM, SecureConstant.RM);
            value = SMHelper.sm4Decrypt(unWrap, sm4key, iv);
        }
        body.put(key, value);
    }

    /**
     * Find request signature
     */
    private String findSignature() {
        // find signatures in forms
        String[] values = params.remove(SecureConstant.SIGN);
        if (values != null && values.length == 1) {
            return values[0];
        }
        // find the signature in the payload
        if (body.containsKey(SecureConstant.SIGN)) {
            return body.remove(SecureConstant.SIGN).toString();
        }
        // Can't find throws exception
        throw new InvalidRequestException(RequestTemplate.REQUEST_SIGNATURE_NOT_FOUNT);
    }

    @Override
    public ServletInputStream getInputStream() {
        return new ServletInputStream() {
            private final ByteBuffer buffer = ByteBuffer.wrap(JSONUtil.toJsonStr(body).getBytes());

            @Override
            public int read() {
                return (isFinished() ? -1 : buffer.get()) & 0xFF;
            }

            @Override
            public boolean isFinished() {
                return buffer.remaining() == 0;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener listener) {
                // implements too hard, ignore
                throw new UnsupportedOperationException();
            }
        };
    }

    @Override
    public String getParameter(String name) {
        String[] array = params.get(name);
        return array == null || array.length == 0 ? null : array[0];
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        return Collections.unmodifiableMap(params);
    }

    @Override
    public Enumeration<String> getParameterNames() {
        return Collections.enumeration(params.keySet());
    }

    @Override
    public String[] getParameterValues(String name) {
        return params.get(name);
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    /**
     * Verify that the signature matches the data
     *
     * @return match returns true
     */
    boolean compareSign() {
        StringBuilder sb = new StringBuilder();
        // Generate params signature
        for (String[] values : params.values()) {
            if (ArrayUtil.isEmpty(values)) {
                continue;
            }
            if (values.length > 1) {
                sb.append(JSONUtil.toJsonStr(values));
                continue;
            }
            sb.append(values[0]);
        }
        // Generate body signature
        for (Object value : body.values()) {
            sb.append(value);
        }
        log.debug("secure join: {}", sb);
        // check sign
        return SMHelper.sm3Hash(sb + timestamp).equals(signature);
    }

    /**
     * request timestamp
     */
    long getTimestamp() {
        return Long.parseLong(timestamp, 16);
    }

    /**
     * request signature
     */
    String sign() {
        return signature;
    }
}
